Skip to main content

Charts

The Blotch Library has ready-made charts for showing numbers at a glance. They are drawn in black and white, and tell series apart with dashes and fill patterns instead of colors, so they stay readable on an E-ink display. They also switch to light-on-dark automatically when the Frame uses dark mode.

import { LineChart } from "~lib/charts/line-chart";

const Widget = () => <LineChart data={[12, 18, 15, 22, 30]} />;

Every chart is imported from ~lib/charts/<name>. This page covers the three you will reach for most often. The rest are listed at the end.

Charting fetched data​

Charts take plain arrays, so turn the API's response into one with map. Because useFetch waits for the data before your Widget renders, there is nothing to check first:

import { LineChart } from "~lib/charts/line-chart";
import { useFetch } from "~lib/use-fetch";

type Reading = {
readonly time: string;
readonly temperature: number;
};

const Widget = () => {
const { data } = useFetch<Reading[]>("https://api.example.com/readings");

return (
<LineChart
data={data.map((reading): [Date, number] => [
new Date(reading.time),
reading.temperature,
])}
axis={{ x: { visible: true }, y: { visible: true, name: "°C" } }}
width="100%"
height="100%"
/>
);
};

BarIndicator​

A horizontal bar filled to show how far one number is towards another, such as storage used or a goal reached.

import { BarIndicator } from "~lib/charts/bar-indicator";

<BarIndicator numerator={75} denominator={100} />;

The fill stays between empty and full: a numerator above denominator shows a full bar, and a negative one shows an empty bar.

PropTypeRequiredDefaultDescription
numeratornumberYesThe current value.
denominatornumberYesThe value of a full bar.
widthnumber | stringNo"100%"The bar's width. See Sizing.
heightnumber | stringNoA slim barThe bar's height. See Sizing.

DonutGauge​

A ring filled to show one number as a share of another, such as battery level or a percentage.

import { DonutGauge } from "~lib/charts/donut-gauge";

<DonutGauge numerator={7} denominator={10} pattern="diagonal" />;
PropTypeRequiredDefaultDescription
numeratornumberYesThe current value.
denominatornumberYesThe value of a full ring.
pattern"solid" | "diagonal" | "dots" | "crosshatch"No"solid"How the filled part of the ring is drawn.
widthnumber | stringNo300See Sizing.
heightnumber | stringNo300See Sizing.

LineChart​

Plots values as a connected line, for trends over time.

import { LineChart } from "~lib/charts/line-chart";

<LineChart data={[12, 18, 15, 22, 30]} />;

A chart needs at least two points. With fewer, it draws nothing.

Points​

data accepts whichever shape is closest to what your API returns:

ShapeExampleNotes
number[][12, 18, 15]Values only. They are spaced evenly.
[number, number][][[0, 12], [5, 18], [7, 15]][x, y] pairs. Points can be spaced unevenly.
{ x, y }[][{ x: 0, y: 12 }, { x: 5, y: 18 }]The same as pairs, as objects.
[Date, number][][[new Date("2025-01-01"), 12], ...]Dates along the x axis. The axis labels become dates.
[x, y, extra][][[new Date("2025-01-01"), 12, "☀️"]]A third item of your own, available to dataLabel.

If your dates are timestamps in milliseconds rather than Date objects, set axis.x.type to "date" to get date labels.

Several lines​

Pass an object instead of an array to draw one line per key. Each line gets its own dash style, and legend names them:

<LineChart
data={{
London: [12, 14, 13, 17],
Paris: [15, 16, 18, 19],
}}
legend={{ position: "top-right" }}
/>

Set stacked to draw each line on top of the ones before it, so the top line shows the total.

Axes​

Axes are hidden by default. Turn each on with visible, and label it with name:

<LineChart
data={[
[new Date("2025-01-01"), 120],
[new Date("2025-02-01"), 135],
[new Date("2025-03-01"), 128],
]}
axis={{
x: { visible: true, dateFormat: { month: "short" } },
y: { visible: true, name: "Visitors", min: 0, grid: true },
}}
/>
OptionApplies toDescription
visiblex, yShows the axis and its labels.
namex, yA title along the axis.
gridx, yDraws dotted grid lines across the chart.
minyWhere the axis starts. It otherwise starts at the lowest value, so set 0 to stop a small change looking like a big one.
typexSet to "date" when the x values are timestamps in milliseconds.
dateFormatxHow dates are written, as Intl.DateTimeFormat options.

Labelling points​

dataLabel puts something next to each point. It receives the point, its position in the list and the name of its line:

const days: [Date, number, string][] = [
[new Date("2025-06-01"), 21, "☀️"],
[new Date("2025-06-02"), 17, "🌧️"],
[new Date("2025-06-03"), 19, "⛅"],
];

<LineChart data={days} dots dataLabel={(_point, index) => days[index][2]} />;

dataIcon sets an icon above a point's label, at a size that reads on its own. Return the icon's name, or null for none:

const hours: [Date, number, string][] = [
[new Date("2025-06-01T14:00"), 21, "sun"],
[new Date("2025-06-01T15:00"), 19, "cloud-rain"],
];

<LineChart
data={hours}
dots
axis={{ y: { max: 30 } }}
dataLabel={(point) => `${point[1]}°`}
dataIcon={(point) => point[2]}
/>;

Give the line some room above it with axis.y.max, so every icon fits over its point.

Props​

PropTypeRequiredDefaultDescription
dataPoints, or an object of points per lineYesSee Points and Several lines.
axis{ x?: {...}, y?: {...} }NoHiddenSee Axes.
widthnumber | stringNo300See Sizing.
heightnumber | stringNo300See Sizing.
smoothbooleanNofalseDraws curves between points instead of straight lines.
dotsbooleanNofalseMarks each point with a dot.
stackedbooleanNofalseStacks each line on top of the ones before it.
legendboolean | { position: "top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right" }NoNo legendNames each line. Only shown when there is more than one line. true places it at the bottom.
direction"horizontal" | "vertical"No"horizontal""vertical" turns the chart on its side.
dataLabel(point, index, lineName) => ReactNodeNoSee Labelling points.
dataIcon(point, index, lineName) => string | nullNoAn icon's name to set above a point. See Labelling points.

Sizing​

width and height take a number of pixels (200) or any CSS size ("100%", "12rem"). To have a chart fill the space it is given, set both to "100%", which keeps it looking right as the Widget is resized.

A chart set to "100%" also fills its box in every preview of your Widget. Lines, areas and bars stretch to the box, while text, dots and patterns stay the same size. Round charts, such as PieChart and DonutGauge, keep their shape and grow or shrink to fit instead.

More charts​

These are also in the Blotch Library. Each link opens its live examples and full list of props.

ChartImportGood for
AreaChart~lib/charts/area-chartA line with the area below it filled.
BarChart~lib/charts/bar-chartComparing values side by side, upright or on its side.
CandlestickChart~lib/charts/candlestick-chartOpen, high, low and close prices.
ContributionGrid~lib/charts/contribution-gridA year of activity, a square per day.
FunnelChart~lib/charts/funnel-chartStages that narrow, such as sign-ups becoming customers.
Heatmap~lib/charts/heatmapIntensity across a grid, such as activity per day.
PieChart~lib/charts/pie-chartParts of a whole.
RadarChart~lib/charts/radar-chartComparing several measures at once.
SankeyChart~lib/charts/sankey-chartAmounts flowing from one place to another.
ScatterPlot~lib/charts/scatter-plotIndividual points, without a line joining them.
Sparkline~lib/charts/sparklineA tiny trend line with no axes, to sit beside a number.
SurfaceChart~lib/charts/surface-chartA 3D surface of values over a grid.
TreeMap~lib/charts/tree-mapParts of a whole, as nested rectangles sized by value.